package com.gy.controller.assistant.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONArray;
import com.gy.File.fileInfo;
import com.gy.config.AliOSSConfig;
import com.gy.config.BaiduOcrConfig;
import com.gy.controller.assistant.extractor.XiaoNingExtractor;
import com.gy.controller.assistant.tools.PaperTool;
import com.gy.controller.common.UserHolder;
import com.gy.entity.research.Horizontal;
import com.gy.entity.research.Longitudinal;
import com.gy.entity.result.*;
import com.gy.service.FileInfoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * PDF提取工具类（专注学生获奖场景）
 * 核心能力：从OSS存储的"PDF转图片"中提取OCR文本 → 调用AI解析学生获奖字段（StudentAward）
 */
@Component
public class PdfExtractUtil {
    // 日志实例（固定命名，便于日志排查）
    private static final Logger log = LoggerFactory.getLogger(PdfExtractUtil.class);
    // 图片下载超时时间（毫秒，可配置化，此处暂用常量）
    private static final int IMAGE_DOWNLOAD_TIMEOUT = 5000;
    // HTTPS前缀（统一OSS图片URL格式）
    private static final String HTTPS_PREFIX = "https://";
    // 学生获奖默认同步状态（与前端默认值保持一致）
    private static final String DEFAULT_SYNC_STATUS = "是";

    // 新增：横向课题时间格式（适配OCR识别的常见格式）
    private static final List<SimpleDateFormat> HORIZONTAL_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd")
    );

    // 新增：纵向课题时间格式（适配OCR识别的常见格式）
    private static final List<SimpleDateFormat> LONGITUDINAL_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd")
    );

    // 新增：纵向课题合法枚举值（用于校验）
    private static final List<String> VALID_PROJECT_TYPES = List.of("自然科学类", "社会科学类", "教育教学类", "其他");
    private static final List<String> VALID_PROJECT_LEVELS = List.of("国家级", "省部级", "市厅级", "校级", "院级");
    private static final List<String> VALID_PROJECT_STATUSES = List.of("在研", "已结题", "暂停", "终止");

    // 新增：专利日期格式（适配OCR常见格式：yyyy年MM月dd日、yyyy-MM-dd等）
    private static final List<SimpleDateFormat> PATENT_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd")
    );
    // 新增：专利合法枚举值（用于校验）
    private static final List<String> VALID_PATENT_TYPES = List.of("发明专利", "实用新型专利", "外观设计专利");
    private static final List<String> VALID_PATENT_STATUSES = List.of("在审", "授权", "失效", "驳回");
    // 新增：软件著作权日期格式（适配证书常见格式）
    private static final List<SimpleDateFormat> COPYRIGHT_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd")
    );
    // 新增：软件著作权证书号正则（匹配“软著登字第XXXX号”格式）
    private static final Pattern CERTIFICATE_NO_PATTERN = Pattern.compile("软著登字第\\d+号");


    // 新增：著作日期格式（适配版权页常见格式）
    private static final List<SimpleDateFormat> PUBLICATION_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd"),
            new SimpleDateFormat("yyyy年MM月") // 兼容仅标注年月的场景
    );
    // 新增：ISBN正则（匹配10位/13位ISBN，含分隔符）
    private static final Pattern ISBN_PATTERN = Pattern.compile("(978|979)[-\\s]?\\d{1,5}[-\\s]?\\d{1,7}[-\\s]?\\d{1,6}[-\\s]?\\d");
    // 新增：著作合法枚举值（用于校验）
    private static final List<String> VALID_ROLES = List.of("著作", "编著", "译著", "其他");
    private static final List<String> VALID_TYPES = List.of("非教材", "普通教材", "省级规划教材", "国家级规划教材");

    // 新增：继续教育材料日期格式（适配证书常见格式）
    private static final List<SimpleDateFormat> CEM_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd"),
            new SimpleDateFormat("yyyy年MM月") // 兼容仅标注年月的场景
    );
    // 新增：课时提取正则（匹配数字+单位，如“8课时”“4.5小时”“3天（24课时）”）
    private static final Pattern HOURS_PATTERN = Pattern.compile("\\d+(\\.\\d+)?");


    // 新增：档案资料日期格式（适配证书常见格式）
    private static final List<SimpleDateFormat> CERT_DATE_FORMATS = List.of(
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy年MM月dd日"),
            new SimpleDateFormat("yyyy.MM.dd"),
            new SimpleDateFormat("yyyy年MM月") // 兼容仅标注年月的场景
    );
    // 新增：档案资料合法枚举值（用于证书类型校验）
    private static final List<String> VALID_CERT_TYPES = List.of(
            "学历证书", "学位证书", "职业资格证书", "荣誉证书", "培训证书", "其他"
    );
    // 新增：证书编号正则（保留字母、数字、短横线，过滤空格/乱码）
    private static final Pattern CERT_NUM_PATTERN = Pattern.compile("[A-Za-z0-9\\-]{5,20}");

    // -------------------------- 依赖注入（保留原有核心依赖） --------------------------
    /** 文件信息服务：查询PDF图片的页数、存储路径 */
    @Autowired
    private FileInfoService fileInfoService;

    /** OSS配置：获取Bucket名称、Endpoint，用于生成图片URL */
    @Autowired
    private AliOSSConfig aliOSSConfig;

    /** AI提取接口：调用小宁Agent解析学生获奖字段 */
    @Autowired
    private XiaoNingExtractor xiaoNingExtractor;

    /** 百度OCR工具类：用于图片文本识别（若实际类名为BaiduOcrUtil，需替换此处类名） */
    @Autowired
    private BaiduOcrConfig baiduOcrUtil;

    @Autowired
    private PaperTool paperTool;


    // ========================== 核心方法：从PDF图片提取OCR文本 ==========================
    /**
     * 从"PDF转换后的图片列表"中提取合并OCR文本
     * @param filePath 前端传递的imageBasePath（格式：pdf-images/xxx-uuid/）
     * @param baiduOcrUtil 百度OCR工具类实例（与成员变量保持一致，可简化为直接用成员变量）
     * @return 所有图片的合并OCR文本（去重、去空）
     * @throws Exception 路径解码失败、文件不存在、图片下载/识别异常
     */
    public String extractPdfTextFromImages(String filePath, BaiduOcrConfig baiduOcrUtil) throws Exception {
        // 1. 前置校验：filePath不能为空
        if (filePath == null || filePath.trim().isEmpty()) {
            throw new IllegalArgumentException("PDF图片路径（filePath）不能为空");
        }

        // 2. 解码filePath：处理前端URL编码，提取OSS相对路径
        String decodedFilePath = decodeFilePath(filePath);
        log.info("PDF图片路径解码完成：原路径={}，解码后路径={}", filePath, decodedFilePath);

        // 3. 查询文件信息：获取图片总页数（PDF转图片后每页对应1张图）
        fileInfo fileInfo = fileInfoService.getByFilePath(decodedFilePath);
        if (fileInfo == null) {
            throw new IllegalArgumentException("未查询到文件信息，filePath=" + decodedFilePath);
        }
        int pageCount = fileInfo.getPageCount();
        if (pageCount <= 0) {
            throw new IllegalArgumentException("文件页数异常，pageCount=" + pageCount + "（需大于0）");
        }
        log.info("获取PDF图片总页数：{}页", pageCount);

        // 4. 生成所有图片的OSS完整URL（page-1.jpg ~ page-N.jpg）
        List<String> imageUrls = generateImageUrls(decodedFilePath, pageCount);
        log.debug("生成图片URL列表：共{}个URL，示例={}", imageUrls.size(), imageUrls.get(0));

        // 5. 遍历图片URL，下载并调用OCR识别文本
        StringBuilder totalOcrText = new StringBuilder();
        for (int i = 0; i < imageUrls.size(); i++) {
            String imageUrl = imageUrls.get(i);
            int currentPage = i + 1; // 页码从1开始

            try {
                // 5.1 下载图片：转为字节数组（适配百度OCR输入格式）
                byte[] imageBytes = downloadImage(imageUrl);
                if (imageBytes == null || imageBytes.length == 0) {
                    log.warn("第{}页图片下载为空，跳过该页，URL={}", currentPage, imageUrl);
                    continue;
                }

                // 5.2 校验OCR工具类：避免空指针
                if (baiduOcrUtil == null) {
                    throw new NullPointerException("百度OCR工具类未注入（baiduOcrUtil is null）");
                }

                // 5.3 调用OCR识别当前页文本
                String pageOcrText = baiduOcrUtil.recognizeImageBytes(imageBytes);
                if (pageOcrText != null && !pageOcrText.trim().isEmpty()) {
                    totalOcrText.append(pageOcrText).append("\n"); // 分页符分隔，便于后续解析
                    log.debug("第{}页OCR识别完成，文本长度={}字符", currentPage, pageOcrText.length());
                } else {
                    log.warn("第{}页OCR识别结果为空，URL={}", currentPage, imageUrl);
                }

            } catch (Exception e) {
                log.error("第{}页图片处理失败（URL={}）", currentPage, imageUrl, e);
                // 可选：若需严格处理，取消注释下方代码（识别失败则中断整体流程）
                // throw new Exception("第" + currentPage + "页图片OCR识别失败：" + e.getMessage(), e);
            }
        }

        // 6. 处理最终文本：去重空行、trim
        String finalOcrText = totalOcrText.toString().replaceAll("\\n+", "\n").trim();
        log.info("所有图片OCR合并完成，总文本长度={}字符", finalOcrText.length());
        return finalOcrText;
    }


    // ========================== 辅助方法：工具类逻辑（保留原有实现，优化日志） ==========================
    /**
     * 解码前端传递的filePath：处理URL编码 + 提取OSS相对路径
     * @param filePath 前端传递的原始路径（可能含URL编码，如%2F代替/）
     * @return 解码后的OSS相对路径（格式：pdf-images/xxx-uuid/）
     */
    private String decodeFilePath(String filePath) throws Exception {
        // 1. URL解码（处理前端编码的特殊字符）
        String decodedPath = URLDecoder.decode(filePath, StandardCharsets.UTF_8.name());
        log.debug("URL解码前：{}，解码后：{}", filePath, decodedPath);

        // 2. 提取OSS相对路径（若前端传递完整URL，如https://xxx/pdf-images/xxx/，则截取后半段）
        if (decodedPath.startsWith(HTTPS_PREFIX)) {
            Pattern ossPathPattern = Pattern.compile("pdf-images/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/");
            Matcher matcher = ossPathPattern.matcher(decodedPath);
            if (matcher.find()) {
                decodedPath = matcher.group(0);
                log.debug("从完整URL提取OSS相对路径：{}", decodedPath);
            } else {
                throw new IllegalArgumentException("无法从URL提取OSS路径，URL=" + decodedPath);
            }
        }

        // 3. 确保路径以/结尾（与数据库存储的filePath格式保持一致）
        if (!decodedPath.endsWith("/")) {
            decodedPath += "/";
            log.debug("路径补全/：{}", decodedPath);
        }

        return decodedPath;
    }

    /**
     * 生成OSS图片完整URL列表
     * @param decodedFilePath 解码后的OSS相对路径（pdf-images/xxx-uuid/）
     * @param pageCount 图片总页数
     * @return 完整OSS图片URL列表（如：https://bucket.endpoint/pdf-images/xxx/page-1.jpg）
     */
    private List<String> generateImageUrls(String decodedFilePath, int pageCount) {
        List<String> imageUrls = new ArrayList<>(pageCount);
        // 拼接OSS基础域名（格式：https://bucketName.endpoint/）
        String ossBaseDomain = HTTPS_PREFIX + aliOSSConfig.getBucketName() + "." + aliOSSConfig.getEndpoint() + "/";

        for (int i = 1; i <= pageCount; i++) {
            // 图片命名规则：相对路径 + page-N.jpg（与PDF转图片时的命名保持一致）
            String imageUrl = ossBaseDomain + decodedFilePath + "page-" + i + ".jpg";
            imageUrls.add(imageUrl);
        }
        return imageUrls;
    }

    /**
     * 从OSS URL下载图片，转为字节数组（适配百度OCR输入）
     * @param imageUrl OSS图片完整URL
     * @return 图片字节数组（JPG格式）
     */
    private byte[] downloadImage(String imageUrl) throws Exception {
        URL url = new URL(imageUrl);
        URLConnection connection = url.openConnection();

        // 设置超时时间（避免长时间阻塞）
        connection.setConnectTimeout(IMAGE_DOWNLOAD_TIMEOUT);
        connection.setReadTimeout(IMAGE_DOWNLOAD_TIMEOUT);

        try (InputStream inputStream = connection.getInputStream()) {
            // 校验图片格式（仅支持可解析的图片格式）
            BufferedImage image = ImageIO.read(inputStream);
            if (image == null) {
                throw new Exception("图片格式不可解析（可能为损坏文件），URL=" + imageUrl);
            }

            // 转为JPG格式字节数组（统一OCR输入格式）
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(image, "jpg", outputStream);
            byte[] imageBytes = outputStream.toByteArray();

            log.debug("图片下载完成，URL={}，字节大小={}KB", imageUrl, imageBytes.length / 1024);
            return imageBytes;

        } catch (Exception e) {
            throw new Exception("图片下载失败（URL=" + imageUrl + "）", e);
        }
    }


    // ========================== AI提取方法：学生获奖字段解析（保留核心逻辑，优化容错） ==========================
    /**
     * 调用AI（XiaoNingExtractor）从OCR文本中提取学生获奖字段，生成StudentAward实体
     * @param ocrText 合并后的OCR文本（来自extractPdfTextFromImages方法）
     * @return 填充后的StudentAward（缺失字段用默认值）
     */
    public StudentAward extractStudentAwardByAi(String ocrText) {
        // 1. 前置校验：OCR文本为空则返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取学生获奖字段：OCR文本为空，返回默认实体");
            return buildDefaultStudentAward();
        }

        try {
            // 2. 生成临时MemoryId：避免AI上下文冲突（UUID保证唯一性）
            String tempMemoryId = "student-award-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取学生获奖字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令：明确提取目标，引导AI返回标准JSON
            String aiUserPrompt = String.format("""
                    请严格按照System Prompt中「学生获奖表单字段提取规则」，从以下OCR文本中提取字段，仅返回JSON字符串（无多余文本）：
                    1. 必须提取的字段：title（比赛名称）、awardStudents（获奖学生）、awardLevel（获奖名次）、level（赛事级别）、grantingUnit（授予单位）、awardTime（yyyy-MM-dd）、authorRank（数字）；
                    2. 缺失字段赋值为null（字符串字段null，数字字段null）；
                    3. OCR文本：%s
                    """, ocrText);

            // 4. 调用AI提取：获取JSON响应
            log.info("AI提取学生获奖字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractStudentAwardFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应：为空则返回默认实体
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取学生获奖字段：AI返回空响应");
                return buildDefaultStudentAward();
            }

            // 6. 清理AI响应：去除前后引号、转义字符，确保JSON格式合法
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "") // 去除前后引号（如AI返回"{}" → {}）
                    .replaceAll("\\\\n|\\\\r", "")    // 去除转义换行符
                    .replaceAll("\\\\", "");         // 去除多余转义符（如\\\" → "）
            log.debug("AI提取学生获奖字段：清理后JSON={}", cleanAiJson);

            // 7. JSON解析为StudentAward：使用Hutool JSONUtil，简化解析逻辑
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回内容不是合法JSON：" + cleanAiJson);
            }
            StudentAward studentAward = JSONUtil.toBean(cleanAiJson, StudentAward.class);

            // 8. 补全默认值：AI未返回的字段用业务默认值
            studentAward = fillStudentAwardDefaultValue(studentAward);

            log.info("AI提取学生获奖字段成功：比赛名称={}，获奖学生={}，获奖名次={}",
                    studentAward.getTitle(), studentAward.getAwardStudents(), studentAward.getAwardLevel());
            return studentAward;

        } catch (Exception e) {
            log.error("AI提取学生获奖字段失败", e);
            // 容错：解析异常时返回默认实体，避免前端报错
            return buildDefaultStudentAward();
        }
    }

    /**
     * 构建学生获奖默认实体（容错用）
     */
    private StudentAward buildDefaultStudentAward() {
        StudentAward defaultAward = new StudentAward();
        defaultAward.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        defaultAward.setAuthorInfo(new ArrayList<>());    // 默认空作者列表
        defaultAward.setRemarks("");                     // 默认空备注
        log.debug("构建学生获奖默认实体：{}", defaultAward);
        return defaultAward;
    }

    /**
     * 补全StudentAward缺失字段的默认值
     * @param studentAward AI解析后的实体
     * @return 补全后的实体
     */
    private StudentAward fillStudentAwardDefaultValue(StudentAward studentAward) {
        if (studentAward == null) {
            return buildDefaultStudentAward();
        }
        // 同步状态：默认"是"
        if (studentAward.getSyncStatus() == null) {
            studentAward.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 作者列表：默认空列表（避免NullPointerException）
        if (studentAward.getAuthorInfo() == null) {
            studentAward.setAuthorInfo(new ArrayList<>());
        }
        // 备注：默认空字符串
        if (studentAward.getRemarks() == null) {
            studentAward.setRemarks("");
        }
        // 本人排名：默认null（前端需手动选择，避免错误默认值）
        if (studentAward.getAuthorRank() != null && (studentAward.getAuthorRank() < 1 || studentAward.getAuthorRank() > 13)) {
            studentAward.setAuthorRank(null); // 修正超出范围的排名
        }
        return studentAward;
    }

    // ========================== 新增：论文场景的核心方法（extractPapersByAi） ==========================
    /**
     * 论文场景：从OCR文本中AI提取论文字段（Papers实体）
     * @param ocrText 合并后的OCR文本（建议取前4页：封面+目录+检索报告）
     * @param needCleanText 是否需要清理文本（true=调用PaperTool清理无关内容）
     * @return 填充后的Papers实体（缺失字段用默认值）
     */
    public Papers extractPapersByAi(String ocrText, boolean needCleanText) {
        // 1. 前置校验：OCR文本为空 → 返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取论文字段：OCR文本为空，返回默认实体");
            return buildDefaultPapers();
        }

        // 2. 文本清理（仅当PaperTool注入且需要清理时执行）
        String cleanOcrText = ocrText;
        if (needCleanText && paperTool != null) {
            cleanOcrText = paperTool.cleanPaperOcrText(ocrText);
            log.debug("论文OCR文本清理完成：原长度={} → 新长度={}", ocrText.length(), cleanOcrText.length());
        }

        try {
            // 3. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "papers-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取论文字段：临时MemoryId={}", tempMemoryId);

            // 4. 构造AI请求指令（明确论文字段提取规则）
            String aiUserPrompt = String.format("""
                            请严格按照System Prompt中「论文表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                            1. 核心字段：
                               - title（论文标题）、journalName（刊物名称）、publishDate（yyyy-MM-dd）
                               - indexing（收录检索数组，如["SCI","EI"]）、doi（DOI号）、pageRange（页码，如"21-24"）
                               - impactFactor（影响因子，数字，如3.102）、volumeNumber（卷号）、issueNumber（期号）
                               - keywords（关键词，从OCR的"关键词""Keywords"提取，用逗号分隔的字符串，如"机器学习,人工智能"）
                               - abstractContent（摘要，从OCR的"摘要""Abstract"提取完整文本，保留段落结构）
                               - authorInfo（作者信息数组，从OCR的"作者""作者列表"提取，每个元素含：
                                   rank：排序（数字字符串，如"1"，按作者顺序）
                                   name：中文名（不可为空）
                                   identity：身份（默认"老师"，可选"博士生/硕士生/本科生/其他"）
                                   isCorresponding：是否通讯作者（布尔值，无则false）
                                   isCoFirstAuthor：是否共同一作（布尔值，无则false）
                                   institution：单位（默认"宁德师范学院信息工程学院"）
                                 无作者信息则返回空数组[]）
                            2. 格式要求：
                               - 关键词为逗号分隔的字符串（非数组），摘要为原始文本
                               - 时间统一yyyy-MM-dd，数组字段必为数组（空则[]），布尔值用true/false
                               - 缺失字段：字符串为null，数字为null，数组为空数组
                            3. OCR文本：%s
                    """, cleanOcrText);

            // 5. 调用AI提取（注意：需确保XiaoNingExtractor有extractPapersFields方法）
            log.info("AI提取论文字段：开始调用，OCR文本长度={}字符", cleanOcrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractPapersFields(tempMemoryId, aiUserPrompt);

            // 6. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取论文字段：AI返回空响应");
                return buildDefaultPapers();
            }

            // 7. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取论文字段：清理后JSON={}", cleanAiJson);

            // 8. 解析JSON为Papers实体
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            Papers papers = JSONUtil.toBean(cleanAiJson, Papers.class);

            // 9. 校验并补全字段（调用PaperTool）
            if (paperTool != null) {
                papers = validateAndFixPapers(papers);
            }

            // 10. 补全默认值
            papers = fillPapersDefaultValue(papers);

            log.info("AI提取论文字段成功：标题={}，刊物={}", papers.getTitle(), papers.getJournalName());
            return papers;

        } catch (Exception e) {
            log.error("AI提取论文字段失败", e);
            return buildDefaultPapers();
        }
    }

    /**
     * 论文辅助：校验字段合法性（调用PaperTool）
     */
    private Papers validateAndFixPapers(Papers papers) {
        if (papers == null) return buildDefaultPapers();

        // 1. 校验收录检索（确保是合法选项）
        if (papers.getIndexing() != null) {
            papers.setIndexing(paperTool.validatePaperIndexing(papers.getIndexing()));
        }

        // 2. 校验DOI格式
        if (papers.getDoi() != null) {
            papers.setDoi(paperTool.validateDoi(papers.getDoi()));
        }

        // 3. 校验影响因子（转为BigDecimal）
        if (papers.getImpactFactor() != null) {
            String validIf = paperTool.validateImpactFactor(papers.getImpactFactor().toString());
            papers.setImpactFactor(validIf != null ? new BigDecimal(validIf) : null);
        }


        // 新增：处理关键词（统一用逗号分隔，去除空格）
        if (papers.getKeywords() != null) {
            // 替换中文逗号、分号为英文逗号，去除首尾空格和多余空格
            String cleanedKeywords = papers.getKeywords()
                    .replaceAll("[，;；]", ",")
                    .replaceAll("\\s+,\\s+", ",")
                    .trim();
            papers.setKeywords(cleanedKeywords);
        }

        // 新增：处理摘要（去除多余空行和空格，保留有效段落）
        if (papers.getAbstractContent() != null) {
            // 合并连续空行为单个换行，去除首尾空格
            String cleanedAbstract = papers.getAbstractContent()
                    .replaceAll("\\n+", "\n")
                    .replaceAll("\\s+", " ") // 多个空格合并为一个
                    .trim();
            papers.setAbstractContent(cleanedAbstract);
        }

        // 4. 修复作者信息（补默认值）
        if (papers.getAuthorInfo() == null) {
            papers.setAuthorInfo(new ArrayList<>());
        } else {
            List<Author> validAuthors = new ArrayList<>();
            for (Author author : papers.getAuthorInfo()) {
                if (author == null || author.getName() == null || author.getName().trim().isEmpty()) {
                    continue; // 过滤无姓名的无效作者
                }
                // 补全默认值
                if (author.getRank() == null || String.valueOf(author.getRank()).trim().isEmpty()) {
                    author.setRank(validAuthors.size() + 1);// 自动补排序
                }
                if (author.getIdentity() == null || author.getIdentity().trim().isEmpty()) {
                    author.setIdentity("老师"); // 默认身份
                }
                if (author.getInstitution() == null || author.getInstitution().trim().isEmpty()) {
                    author.setInstitution("信息工程学院"); // 默认单位
                }
                validAuthors.add(author);
            }
            // 若过滤后无作者，添加1个默认空作者（避免前端表单校验失败）
            if (validAuthors.isEmpty()) {
                Author defaultAuthor = new Author();
                defaultAuthor.setRank(1);
                defaultAuthor.setName("");
                defaultAuthor.setIdentity("老师");
                defaultAuthor.setInstitution("信息工程学院");
                validAuthors.add(defaultAuthor);
            }
            papers.setAuthorInfo(validAuthors);
        }
        return papers;
    }

    /**
     * 论文辅助：补全默认值
     */
    private Papers fillPapersDefaultValue(Papers papers) {
        if (papers == null) return buildDefaultPapers();

        if (papers.getSyncStatus() == null) papers.setSyncStatus(DEFAULT_SYNC_STATUS);
        if (papers.getAuthorRank() == null) papers.setAuthorRank(0);
        if (papers.getCorrespondingAuthor() == null) papers.setCorrespondingAuthor(false);
        if (papers.getIndexing() == null) papers.setIndexing(new ArrayList<>());
        if (papers.getAuthorInfo() == null) papers.setAuthorInfo(new ArrayList<>());
        if (papers.getRemarks() == null) papers.setRemarks("");

        return papers;
    }

    /**
     * 论文辅助：构建默认空实体
     */
    private Papers buildDefaultPapers() {
        Papers papers = new Papers();
        papers.setSyncStatus(DEFAULT_SYNC_STATUS);
        papers.setAuthorRank(0);
        papers.setCorrespondingAuthor(false);
        papers.setIndexing(new ArrayList<>());
        papers.setAuthorInfo(new ArrayList<>());
        papers.setRemarks("");
        return papers;
    }

    // ========================== 新增：个人获奖场景的核心方法 ==========================
    /**
     * 个人获奖场景：从OCR文本中AI提取字段，生成PersonalAward实体
     * @param ocrText 合并后的OCR文本（个人获奖证书通常1-2页）
     * @param pdfPath PDF文件路径（从上传组件获取，赋值给PersonalAward）
     * @return 填充后的PersonalAward（含userId、pdfPath等必要字段）
     */
    public PersonalAward extractPersonalAwardByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取个人获奖字段：OCR文本为空，返回默认实体");
            return buildDefaultPersonalAward(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "personal-award-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取个人获奖字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用个人获奖规则）
            String aiUserPrompt = String.format("""
                请严格按照System Prompt中「个人获奖表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                1. 核心字段：
                   - title（荣誉名称）、authorRank（本人排名，数字，如1）、awardLevel（获奖名次，如"一等奖"）
                   - level（级别，如"国家级"）、grantingUnit（授予单位）、awardTime（获奖时间，yyyy-MM-dd）
                   - authorInfo（共同获奖人/作者列表，从OCR的"共同获奖人""获奖者""参与人"提取，每个元素含：
                       rank：排序（数字字符串，如"1"，按获奖人顺序，不可重复）
                       name：中文名（必填，不可为空或"无"）
                       identity：身份（默认"老师"，可选"博士生/硕士生/本科生/其他"）
                       institution：单位（默认"宁德师范学院信息工程学院"）
                     无共同获奖人则返回空数组[]）
                2. 格式要求：
                   - 时间统一yyyy-MM-dd，数组必为数组（空则[]），缺失字段：字符串为null、数字为null、数组为空数组
                   - authorInfo中name不可为空，否则过滤该条数据
                3. OCR文本：%s
        """, ocrText);

            // 4. 调用AI提取（调用XiaoNingExtractor新增的方法）
            log.info("AI提取个人获奖字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractPersonalAwardFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取个人获奖字段：AI返回空响应");
                return buildDefaultPersonalAward(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取个人获奖字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为PersonalAward实体
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            PersonalAward personalAward = JSONUtil.toBean(cleanAiJson, PersonalAward.class);

            // 8. 补全PersonalAward专属字段（AI无法提取的字段）
            personalAward = fillPersonalAwardExclusiveFields(personalAward, pdfPath);

            // 9. 补全默认值（如authorInfo、syncStatus）
            personalAward = fillPersonalAwardDefaultValue(personalAward);

            log.info("AI提取个人获奖字段成功：荣誉名称={}，授予单位={}",
                    personalAward.getTitle(), personalAward.getGrantingUnit());
            return personalAward;

        } catch (Exception e) {
            log.error("AI提取个人获奖字段失败", e);
            return buildDefaultPersonalAward(pdfPath);
        }
    }


    // -------------------------- 个人获奖辅助方法 --------------------------
    /**
     * 补全PersonalAward专属字段（AI无法提取，需手动赋值）
     * @param personalAward AI解析后的实体
     * @param pdfPath PDF文件路径（从前端上传获取）
     * @return 补全后的实体
     */
    private PersonalAward fillPersonalAwardExclusiveFields(PersonalAward personalAward, String pdfPath) {
        if (personalAward == null) {
            personalAward = new PersonalAward();
        }

        // 1. 赋值PDF路径（前端传递，用于关联文件）
        personalAward.setPdfPath(pdfPath);

        // 2. 赋值当前登录用户ID（从UserHolder获取，关联用户）
        if (UserHolder.getUser() != null && UserHolder.getUser().getUserId() != null) {
            personalAward.setUserId(UserHolder.getUser().getUserId());
        }

        // 3. 软删除默认值（0=未删除）
        personalAward.setIsDelete(0);

        // 4. 时间字段默认值（创建时间、更新时间，若后端用MyBatis-Plus自动填充可省略）
        long now = System.currentTimeMillis();
        if (personalAward.getCreatedAt() == null) {
            personalAward.setCreatedAt(new Timestamp(now));
        }
        if (personalAward.getUpdatedAt() == null) {
            personalAward.setUpdatedAt(new Timestamp(now));
        }

        return personalAward;
    }

    /**
     * 补全PersonalAward默认值（AI未返回的字段）
     */
    private PersonalAward fillPersonalAwardDefaultValue(PersonalAward personalAward) {
        if (personalAward == null) {
            return new PersonalAward();
        }

        // 1. 同步状态默认"是"
        if (personalAward.getSyncStatus() == null) {
            personalAward.setSyncStatus(DEFAULT_SYNC_STATUS);
        }

        // 2. 作者列表默认空数组（避免NullPointerException）
        if (personalAward.getAuthorInfo() == null) {
            personalAward.setAuthorInfo(new ArrayList<>());
        } else {
            // 补全作者列表的默认值（身份、单位）
            for (Author author : personalAward.getAuthorInfo()) {
                if (author.getIdentity() == null || author.getIdentity().trim().isEmpty()) {
                    author.setIdentity("老师");
                }
                if (author.getInstitution() == null || author.getInstitution().trim().isEmpty()) {
                    author.setInstitution("信息工程学院");
                }
                if (author.getRank() == null) {
                    author.setRank(1); // 默认排序1
                }
            }
        }

        // 新增：规范作者信息（核心修改）
        if (personalAward.getAuthorInfo() == null) {
            personalAward.setAuthorInfo(new ArrayList<>());
        } else {
            List<Author> validAuthors = new ArrayList<>();
            for (Author author : personalAward.getAuthorInfo()) {
                // 过滤无姓名的无效作者
                if (author == null || author.getName() == null || author.getName().trim().isEmpty()) {
                    continue;
                }
                // 补全默认值+类型适配
                author.setName(author.getName().trim()); // 姓名去空格
                author.setRank(author.getRank() == null ? validAuthors.size() + 1 : author.getRank()); // rank转为字符串
                author.setIdentity(author.getIdentity() == null || author.getIdentity().trim().isEmpty() ? "老师" : author.getIdentity().trim());
                author.setInstitution(author.getInstitution() == null || author.getInstitution().trim().isEmpty() ? "信息工程学院" : author.getInstitution().trim());
                validAuthors.add(author);
            }
            // 若过滤后无作者，添加1个默认作者（避免前端表单校验失败）
            if (validAuthors.isEmpty()) {
                Author defaultAuthor = new Author();
                defaultAuthor.setRank(1);
                defaultAuthor.setName("");
                defaultAuthor.setIdentity("老师");
                defaultAuthor.setInstitution("信息工程学院");
                validAuthors.add(defaultAuthor);
            }
            personalAward.setAuthorInfo(validAuthors);
        }

        // 3. 备注默认空字符串
        if (personalAward.getRemarks() == null) {
            personalAward.setRemarks("");
        }

        return personalAward;
    }

    /**
     * 构建个人获奖默认实体（容错用，含必要字段）
     */
    private PersonalAward buildDefaultPersonalAward(String pdfPath) {
        PersonalAward defaultAward = new PersonalAward();
        // 补全专属字段
        defaultAward.setPdfPath(pdfPath);
        defaultAward.setIsDelete(0);
        defaultAward.setCreatedAt(new Timestamp(System.currentTimeMillis()));
        defaultAward.setUpdatedAt(new Timestamp(System.currentTimeMillis()));
        // 补全默认值
        defaultAward.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultAward.setRemarks("");
        defaultAward.setAuthorInfo(new ArrayList<>());
        // 关联当前用户ID
        if (UserHolder.getUser() != null) {
            defaultAward.setUserId(UserHolder.getUser().getUserId());
        }
        return defaultAward;
    }

    // ========================== 新增：横向课题解析核心方法 ==========================
    /**
     * 横向课题：从OCR文本AI提取字段，生成Horizontal实体
     * @param ocrText OCR文本（优先取合同封面/关键页，含课题名称、合同编号等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的Horizontal（含用户ID、PDF路径等必要字段）
     */
    public Horizontal extractHorizontalByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取横向课题字段：OCR文本为空，返回默认实体");
            return buildDefaultHorizontal(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "horizontal-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取横向课题字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（仅提取负责人作为作者，不包含参加人）
            String aiUserPrompt = String.format("""
        请严格按照System Prompt中「横向课题表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
        1. 核心字段：
           - projectName（课题名称）、contractSource（合同来源）、contractNumber（合同编号）
           - rank（本人排名，数字，无则null）、fundingAmount（到账经费，数字，如"10万"→10）
           - startTime（开始时间，yyyy-MM-dd）、endTime（截止时间，yyyy-MM-dd）
           - projectStatus（课题状态，仅可选：在研/已结题/暂停/终止）
           - authorInfo（作者信息数组，仅从OCR的"负责人" 提取，不包含"参加人"，每个元素含：
               rank：排序（数字字符串，如"1"，按负责人列表顺序）
               name：中文名（不可为空）
               identity：身份（默认"老师"，无需额外识别）
               institution：单位（默认"宁德师范学院信息工程学院"，无需额外识别）
             无负责人信息则返回空数组[]）
        2. 格式要求：
           - 时间统一转为yyyy-MM-dd，经费仅保留数字，authorInfo必须是数组（即使空）
           - 缺失字段：字符串为null，数字为null，数组为空数组
        3. OCR文本：%s
        """, ocrText);

            // 4. 调用AI提取（调用新增的extractHorizontalFields方法）
            log.info("AI提取横向课题字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractHorizontalFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取横向课题字段：AI返回空响应");
                return buildDefaultHorizontal(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取横向课题字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为Horizontal（先转Map处理字段类型，再赋值）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            cn.hutool.json.JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            Horizontal horizontal = convertJsonToHorizontal(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            horizontal = fillHorizontalDefaultValue(horizontal);

            log.info("AI提取横向课题成功：课题名称={}，合同编号={}",
                    horizontal.getProjectName(), horizontal.getContractNumber());
            return horizontal;

        } catch (Exception e) {
            log.error("AI提取横向课题字段失败", e);
            return buildDefaultHorizontal(pdfPath);
        }
    }


    // -------------------------- 横向课题辅助方法 --------------------------
    /**
     * JSON对象转为Horizontal实体（处理数据类型转换）
     */
    private Horizontal convertJsonToHorizontal(cn.hutool.json.JSONObject jsonObj, String pdfPath) {
        Horizontal horizontal = new Horizontal();

        // 1. 基础字段（字符串类型，原有逻辑不变）
        horizontal.setProjectName(jsonObj.getStr("projectName", null));
        horizontal.setContractSource(jsonObj.getStr("contractSource", null));
        horizontal.setContractNumber(jsonObj.getStr("contractNumber", null));
        horizontal.setProjectStatus(jsonObj.getStr("projectStatus", null));
        horizontal.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 数字字段（排名、经费，原有逻辑不变）
        horizontal.setRank(jsonObj.getInt("rank", 1));
        horizontal.setFundingAmount(parseFundingAmount(jsonObj.getStr("fundingAmount")));

        // 3. 时间字段（转换为Timestamp，原有逻辑不变）
        horizontal.setStartTime(parseHorizontalDate(jsonObj.getStr("startTime")));
        horizontal.setEndTime(parseHorizontalDate(jsonObj.getStr("endTime")));

        // 4. 新增：解析authorInfo数组，转成JSON字符串（适配前端接收格式）
        cn.hutool.json.JSONArray authorInfoArray = jsonObj.getJSONArray("authorInfo");
        if (authorInfoArray != null && !authorInfoArray.isEmpty()) {
            // 数组不为空：转成JSON字符串（前端需JSON.parse后渲染）
            horizontal.setAuthorInfo(authorInfoArray.toString());
            log.debug("解析到作者信息：{}", horizontal.getAuthorInfo());
        } else {
            // 数组为空/未返回：设为空数组的JSON字符串（避免前端接收空字符串）
            horizontal.setAuthorInfo("[]");
            log.debug("未解析到作者信息，设为空数组");
        }

        // 5. 专属字段（AI无法提取，原有逻辑不变）
        horizontal.setPdfPath(pdfPath); // PDF路径
        horizontal.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        horizontal.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步

        return horizontal;
    }

    /**
     * 解析到账经费（处理OCR识别的"5万""5.8万元"等格式）
     */
    private BigDecimal parseFundingAmount(String fundingStr) {
        if (fundingStr == null || fundingStr.trim().isEmpty()) {
            return null;
        }
        // 提取数字（含小数），过滤"万、元"等单位
        String numberStr = fundingStr.replaceAll("[^0-9.]", "").trim();
        if (numberStr.isEmpty() || !numberStr.matches("^\\d+(\\.\\d+)?$")) {
            return null;
        }
        return new BigDecimal(numberStr);
    }

    /**
     * 解析横向课题时间（适配OCR常见格式：yyyy-MM-dd、yyyy年MM月dd日等）
     */
    private Timestamp parseHorizontalDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        // 遍历支持的格式，尝试解析
        for (SimpleDateFormat sdf : HORIZONTAL_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                Date date = sdf.parse(dateStr);
                return new Timestamp(date.getTime());
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析横向课题时间：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 补全横向课题默认值
     */
    private Horizontal fillHorizontalDefaultValue(Horizontal horizontal) {
        if (horizontal == null) {
            return new Horizontal();
        }
        // 同步状态默认"是"
        if (horizontal.getSyncStatus() == null) {
            horizontal.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 备注默认空字符串
        if (horizontal.getRemarks() == null) {
            horizontal.setRemarks("");
        }
        // 作者信息默认空字符串（若前端需展示，可补充默认结构）
        if (horizontal.getAuthorInfo() == null) {
            horizontal.setAuthorInfo("");
        }
        // 课题状态默认"在研"（若未识别）
        if (horizontal.getProjectStatus() == null || !List.of("在研", "已结题", "暂停", "终止").contains(horizontal.getProjectStatus())) {
            horizontal.setProjectStatus("在研");
        }
        if (horizontal.getAuthorInfo() == null || horizontal.getAuthorInfo().trim().isEmpty()) {
            horizontal.setAuthorInfo("[]");
        }
        return horizontal;
    }

    /**
     * 构建横向课题默认实体（容错用）
     */
    private Horizontal buildDefaultHorizontal(String pdfPath) {
        Horizontal defaultHorizontal = new Horizontal();
        // 专属字段赋值
        defaultHorizontal.setPdfPath(pdfPath);
        defaultHorizontal.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        // 默认值
        defaultHorizontal.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultHorizontal.setRemarks("");
        defaultHorizontal.setAuthorInfo("");
        defaultHorizontal.setProjectStatus("在研");
        return defaultHorizontal;
    }

    // ========================== 新增：纵向课题解析核心方法 ==========================
    /**
     * 纵向课题：从OCR文本AI提取字段，生成Longitudinal实体
     * @param ocrText OCR文本（优先取申报书封面/立项页，含课题名称、基金信息等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的Longitudinal（含用户ID、PDF路径等必要字段）
     */
    public Longitudinal extractLongitudinalByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取纵向课题字段：OCR文本为空，返回默认实体");
            return buildDefaultLongitudinal(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "longitudinal-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取纵向课题字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用纵向课题规则）
            // 3. 构造AI请求指令（新增authorInfo提取规则，核心修改）
            String aiUserPrompt = String.format("""
        请严格按照System Prompt中「纵向课题表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
        1. 核心字段：
           - projectName（课题名称）、fundName（基金名称）、projectType（课题类型：自然科学类/社会科学类/教育教学类/其他）
           - projectLevel（课题级别：国家级/省部级/市厅级/校级/院级）、projectNumber（课题编号）
           - fundingAmount（经费，数字，如"5万"→5）、startTime（开始时间，yyyy-MM-dd）、endTime（截止时间，yyyy-MM-dd）
           - authorRank（本人排名，数字）、authorInfo（课题成员列表，从OCR的"负责人""参加人""课题组成员"提取，每个元素含：
               rank：排序（数字字符串，如"1"，负责人在前、参加人在后）
               name：中文名（不可为空）
               identity：身份（默认"老师"，可选"博士生/硕士生/本科生/其他"）
               institution：单位（默认"宁德师范学院信息工程学院"）
             无成员信息则返回空数组[]）
        2. 格式要求：
           - 时间统一yyyy-MM-dd，经费仅保留数字，authorInfo必为数组（空则[]）
           - 缺失字段：字符串为null，数字为null，数组为空数组
        3. OCR文本：%s
        """, ocrText);

            // 4. 调用AI提取（调用新增的extractLongitudinalFields方法）
            log.info("AI提取纵向课题字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractLongitudinalFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取纵向课题字段：AI返回空响应");
                return buildDefaultLongitudinal(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取纵向课题字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为Longitudinal（处理字段类型与合法性校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            Longitudinal longitudinal = convertJsonToLongitudinal(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            longitudinal = fillLongitudinalDefaultValue(longitudinal);

            log.info("AI提取纵向课题成功：课题名称={}，基金名称={}",
                    longitudinal.getProjectName(), longitudinal.getFundName());
            return longitudinal;

        } catch (Exception e) {
            log.error("AI提取纵向课题字段失败", e);
            return buildDefaultLongitudinal(pdfPath);
        }
    }


    // -------------------------- 纵向课题辅助方法 --------------------------
    /**
     * JSON对象转为Longitudinal实体（处理类型转换与合法性校验）
     */
    private Longitudinal convertJsonToLongitudinal(JSONObject jsonObj, String pdfPath) {
        Longitudinal longitudinal = new Longitudinal();

        // 1. 基础字符串字段（直接提取，空值为null）
        longitudinal.setProjectName(jsonObj.getStr("projectName", null));
        longitudinal.setFundName(standardizeFundName(jsonObj.getStr("fundName", null))); // 基金名称标准化
        longitudinal.setProjectType(validateEnumValue(jsonObj.getStr("projectType"), VALID_PROJECT_TYPES));
        longitudinal.setGrantingUnit(jsonObj.getStr("grantingUnit", null));
        longitudinal.setProjectLevel(validateEnumValue(jsonObj.getStr("projectLevel"), VALID_PROJECT_LEVELS));
        longitudinal.setProjectNumber(jsonObj.getStr("projectNumber", null));
        longitudinal.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 数字字段（排名、经费，处理格式与空值）
        longitudinal.setRank(jsonObj.getInt("rank", null));
        longitudinal.setFundingAmount(parseFundingAmount(jsonObj.getStr("fundingAmount")));

        // 3. 时间字段（转换为Timestamp，适配多种格式）
        longitudinal.setStartTime(parseLongitudinalDate(jsonObj.getStr("startTime")));
        longitudinal.setEndTime(parseLongitudinalDate(jsonObj.getStr("endTime")));

        // 4. 课题状态（校验合法性，默认"在研"）
        longitudinal.setProjectStatus(validateEnumValue(jsonObj.getStr("projectStatus"), VALID_PROJECT_STATUSES));

        // 5. 专属字段（AI无法提取，手动赋值）
        longitudinal.setPdfPath(pdfPath); // PDF路径（关联文件）
        longitudinal.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        longitudinal.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步

        // 核心修改：解决JSON库类型匹配 + 转换List<Author>为JSON字符串
        try {
            // 若jsonObj是Hutool的JSONObject，先转为FastJSON的JSONArray（匹配parseAuthorInfo参数）
            cn.hutool.json.JSONArray hutoolAuthorArray = jsonObj.getJSONArray("authorInfo");
            com.alibaba.fastjson.JSONArray fastJsonAuthorArray = new com.alibaba.fastjson.JSONArray();

            if (hutoolAuthorArray != null && !hutoolAuthorArray.isEmpty()) {
                // 逐个转换Hutool JSON对象为FastJSON对象
                for (Object obj : hutoolAuthorArray) {
                    cn.hutool.json.JSONObject hutoolObj = (cn.hutool.json.JSONObject) obj;
                    com.alibaba.fastjson.JSONObject fastJsonObj = com.alibaba.fastjson.JSONObject.parseObject(hutoolObj.toString());
                    fastJsonAuthorArray.add(fastJsonObj);
                }
            }

            // 调用原parseAuthorInfo方法获取List<Author>
            List<Author> authorList = parseAuthorInfo(fastJsonAuthorArray);

            // 将List<Author>转为JSON字符串（适配Longitudinal的authorInfo字段类型）
            String authorInfoJson = com.alibaba.fastjson.JSON.toJSONString(authorList);
            longitudinal.setAuthorInfo(authorInfoJson);
        } catch (Exception e) {
            log.error("转换作者信息失败", e);
            // 异常时设置空数组JSON，避免后续解析报错
            longitudinal.setAuthorInfo("[]");
        }

        return longitudinal;
    }



    /**
     * 基金名称标准化（如"国自科"→"国家自然科学基金"）
     */
    private String standardizeFundName(String fundName) {
        if (fundName == null || fundName.trim().isEmpty()) {
            return null;
        }
        fundName = fundName.trim();
        // 常见简称映射（适配OCR识别的简称）
        if (fundName.contains("国自科") || fundName.contains("国家自然科学")) {
            return "国家自然科学基金";
        } else if (fundName.contains("省科技") || fundName.contains("福建省科技")) {
            return "福建省科技厅项目";
        } else if (fundName.contains("省教育") || fundName.contains("福建省教育")) {
            return "福建省教育厅项目";
        } else {
            return fundName;
        }
    }



    /**
     * 解析纵向课题时间（适配多种格式，转为Timestamp）
     */
    private Timestamp parseLongitudinalDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        for (SimpleDateFormat sdf : LONGITUDINAL_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                Date date = sdf.parse(dateStr);
                return new Timestamp(date.getTime());
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析纵向课题时间：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 补全纵向课题默认值（AI未返回或校验失败的字段）
     */
    private Longitudinal fillLongitudinalDefaultValue(Longitudinal longitudinal) {
        if (longitudinal == null) {
            return new Longitudinal();
        }
        // 1. 同步状态默认"是"
        if (longitudinal.getSyncStatus() == null) {
            longitudinal.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 课题状态默认"在研"（未识别或无效值时）
        if (longitudinal.getProjectStatus() == null || !VALID_PROJECT_STATUSES.contains(longitudinal.getProjectStatus())) {
            longitudinal.setProjectStatus("在研");
        }
        // 3. 课题类型默认"其他"（未识别或无效值时）
        if (longitudinal.getProjectType() == null || !VALID_PROJECT_TYPES.contains(longitudinal.getProjectType())) {
            longitudinal.setProjectType("其他");
        }
        // 4. 备注默认空字符串
        if (longitudinal.getRemarks() == null) {
            longitudinal.setRemarks("");
        }
        // 5. 作者信息默认空字符串（前端未要求时）
        if (longitudinal.getAuthorInfo() == null) {
            longitudinal.setAuthorInfo("");
        }
        if (longitudinal.getAuthorInfo() == null || longitudinal.getAuthorInfo().trim().isEmpty() || "[]".equals(longitudinal.getAuthorInfo().trim())) {
            // 构造含1个默认成员的JSON字符串
            Map<String, Object> defaultAuthor = new HashMap<>();
            defaultAuthor.put("rank", 1);
            defaultAuthor.put("name", "");
            defaultAuthor.put("identity", "老师");
            defaultAuthor.put("institution", "信息工程学院");
            longitudinal.setAuthorInfo(JSONUtil.toJsonStr(Collections.singletonList(defaultAuthor)));
        }
        return longitudinal;
    }

    /**
     * 构建纵向课题默认实体（容错用，含必要字段）
     */
    private Longitudinal buildDefaultLongitudinal(String pdfPath) {
        Longitudinal defaultLong = new Longitudinal();
        // 专属字段赋值
        defaultLong.setPdfPath(pdfPath);
        defaultLong.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        // 默认值
        defaultLong.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultLong.setProjectStatus("在研");
        defaultLong.setProjectType("其他");
        defaultLong.setRemarks("");
        defaultLong.setAuthorInfo("");
        return defaultLong;
    }

    // ========================== 新增：专利解析核心方法 ==========================
    /**
     * 专利：从OCR文本AI提取字段，生成Patents实体
     * @param ocrText OCR文本（优先取专利证书封面/申请文件首页，含专利名称、编号、日期等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的Patents（含用户ID、PDF路径等必要字段）
     */
    public Patents extractPatentsByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取专利字段：OCR文本为空，返回默认实体");
            return buildDefaultPatents(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "patents-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取专利字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用专利规则）
            String aiUserPrompt = String.format("""
                请严格按照System Prompt中「专利表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                1. 核心字段：
                   - title（专利名称）、patentType（专利类型：发明专利/实用新型专利/外观设计专利/国际专利）、patentStatus（专利状态：在审/授权/失效/驳回/已受理/已申请）
                   - patentNumber（专利编号）、applicationNumber（申请编号）、applicationDate（申请日，yyyy-MM-dd）、authorizationDate（授权日，yyyy-MM-dd）
                   - patentOwner（专利权人）、authorRank（本人排名，数字）
                   - authorInfo（发明人信息数组，从OCR的"发明人""设计人""申请人"提取，每个元素含：
                       rank：排序（数字字符串，如"1"，按发明人顺序）
                       name：中文名（不可为空）
                       identity：身份（默认"老师"，可选"博士生/硕士生/本科生/其他"）
                       institution：单位（默认"宁德师范学院信息工程学院"）
                     无发明人信息则返回空数组[]）
                2. 格式要求：
                   - 时间统一yyyy-MM-dd，数组必为数组（空则[]），布尔值用true/false
                   - 缺失字段：字符串为null，数字为null，数组为空数组
                3. OCR文本：%s
                """, ocrText);

            // 4. 调用AI提取（调用新增的extractPatentsFields方法）
            log.info("AI提取专利字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractPatentsFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取专利字段：AI返回空响应");
                return buildDefaultPatents(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取专利字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为Patents（处理字段类型与合法性校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            Patents patents = convertJsonToPatents(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            patents = fillPatentsDefaultValue(patents);

            log.info("AI提取专利成功：专利名称={}，专利类型={}，状态={}",
                    patents.getTitle(), patents.getPatentType(), patents.getPatentStatus());
            return patents;

        } catch (Exception e) {
            log.error("AI提取专利字段失败", e);
            return buildDefaultPatents(pdfPath);
        }
    }


    // -------------------------- 专利辅助方法 --------------------------
    /**
     * JSON对象转为Patents实体（处理类型转换与合法性校验）
     */
    private Patents convertJsonToPatents(JSONObject jsonObj, String pdfPath) {
        Patents patents = new Patents();

        // 1. 基础字符串字段（直接提取，空值为null）
        patents.setTitle(jsonObj.getStr("title", null));
        patents.setPatentType(validateEnumValue(jsonObj.getStr("patentType"), VALID_PATENT_TYPES));
        patents.setPatentStatus(validateEnumValue(jsonObj.getStr("patentStatus"), VALID_PATENT_STATUSES));
        patents.setPatentNumber(standardizePatentNumber(jsonObj.getStr("patentNumber"))); // 专利编号标准化
        patents.setPatentOwner(jsonObj.getStr("patentOwner", null));
        patents.setApplicationNumber(standardizePatentNumber(jsonObj.getStr("applicationNumber"))); // 申请编号标准化
        patents.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 数字字段（本人排名，处理空值）
        patents.setAuthorRank(jsonObj.getInt("authorRank", null));

        // 3. 日期字段（转换为Timestamp，适配多种格式）
        patents.setAuthorizationDate(parsePatentDate(jsonObj.getStr("authorizationDate")));
        patents.setApplicationDate(parsePatentDate(jsonObj.getStr("applicationDate")));

        // 4. 发明人信息（数组处理，补全默认值）
        patents.setAuthorInfo(parseAuthorInfo(jsonObj.getJSONArray("authorInfo")));

        // 5. 专属字段（AI无法提取，手动赋值）
        patents.setPdfPath(pdfPath); // PDF路径（关联文件）
        patents.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        patents.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        patents.setIsDelete(0); // 软删除默认“未删除”

        return patents;
    }

    /**
     * 专利编号/申请编号标准化（补全“ZL”前缀，如“202410012345.6”→“ZL202410012345.6”）
     */
    private String standardizePatentNumber(String number) {
        if (number == null || number.trim().isEmpty()) {
            return null;
        }
        String trimmedNumber = number.trim();
        // 授权专利编号补“ZL”前缀，申请号保持原样
        if (!trimmedNumber.startsWith("ZL") && !trimmedNumber.startsWith("20")) {
            return "ZL" + trimmedNumber;
        }
        return trimmedNumber;
    }

    /**
     * 解析专利日期（适配多种格式，转为Timestamp）
     */
    private Timestamp parsePatentDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        for (SimpleDateFormat sdf : PATENT_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                Date date = sdf.parse(dateStr);
                return new Timestamp(date.getTime());
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析专利日期：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 解析发明人信息（补全身份、单位默认值）
     */
    private List<Author> parseAuthorInfo(cn.hutool.json.JSONArray authorJsonArray) {
        List<Author> authorList = new ArrayList<>();
        if (authorJsonArray == null || authorJsonArray.isEmpty()) {
            return authorList;
        }

        for (int i = 0; i < authorJsonArray.size(); i++) {
            JSONObject authorJson = JSONUtil.parseObj(authorJsonArray.get(i));
            Author author = new Author();
            // 修复：rank从字符串获取，默认取当前索引+1（避免AI返回空）
            Integer rank = authorJson.getInt("rank", i + 1);
            author.setRank(rank); // 确保rank是字符串，与前端匹配
            author.setName(authorJson.getStr("name", "").trim()); // 去空格，避免空姓名
            // 补全默认值（与前端一致）
            author.setIdentity(authorJson.getStr("identity", "老师"));
            author.setInstitution(authorJson.getStr("institution", "信息工程学院"));
            // 过滤无姓名的无效作者
            if (!author.getName().isEmpty()) {
                authorList.add(author);
            }
        }
        return authorList;
    }

    /**
     * 校验枚举值合法性（如专利类型、状态），无效值返回null
     */
    private String validateEnumValue(String value, List<String> validValues) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        String trimmedValue = value.trim();
        return validValues.contains(trimmedValue) ? trimmedValue : null;
    }

    /**
     * 补全专利默认值（AI未返回或校验失败的字段）
     */
    private Patents fillPatentsDefaultValue(Patents patents) {
        if (patents == null) {
            return new Patents();
        }
        // 1. 同步状态默认“是”
        if (patents.getSyncStatus() == null) {
            patents.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 专利类型默认“其他”（未识别或无效值时）
        if (patents.getPatentType() == null || !VALID_PATENT_TYPES.contains(patents.getPatentType())) {
            patents.setPatentType(""); // 空字符串由前端手动选择
        }
        // 3. 专利状态默认“在审”（未识别或无效值时）
        if (patents.getPatentStatus() == null || !VALID_PATENT_STATUSES.contains(patents.getPatentStatus())) {
            patents.setPatentStatus("在审");
        }
        // 4. 发明人信息默认空数组（避免NullPointerException）
        if (patents.getAuthorInfo() == null) {
            patents.setAuthorInfo(new ArrayList<>());
        }
        // 5. 备注默认空字符串
        if (patents.getRemarks() == null) {
            patents.setRemarks("");
        }
        // 6. 关联课题默认null（前端手动选择）
        if (patents.getProjectId() == null) {
            patents.setProjectId(0); // 或null，根据数据库字段类型调整
        }
        if (patents.getAuthorInfo() == null || patents.getAuthorInfo().isEmpty()) {
            List<Author> defaultAuthors = new ArrayList<>();
            Author defaultAuthor = new Author();
            defaultAuthor.setRank(1);
            defaultAuthor.setName("");
            defaultAuthor.setIdentity("老师");
            defaultAuthor.setInstitution("信息工程学院");
            defaultAuthors.add(defaultAuthor);
            patents.setAuthorInfo(defaultAuthors);
        }
        return patents;
    }

    /**
     * 构建专利默认实体（容错用，含必要字段）
     */
    private Patents buildDefaultPatents(String pdfPath) {
        Patents defaultPatent = new Patents();
        // 专属字段赋值
        defaultPatent.setPdfPath(pdfPath);
        defaultPatent.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        defaultPatent.setIsDelete(0);
        // 默认值
        defaultPatent.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultPatent.setPatentStatus("在审");
        defaultPatent.setAuthorInfo(new ArrayList<>());
        defaultPatent.setRemarks("");
        defaultPatent.setProjectId(0);
        return defaultPatent;
    }

    // ========================== 新增：软件著作权解析核心方法 ==========================
    /**
     * 软件著作权：从OCR文本AI提取字段，生成CopyRight实体
     * @param ocrText OCR文本（优先取软件著作权证书封面/核心页，含名称、证书号、日期等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的CopyRight（含用户ID、PDF路径等必要字段）
     */
    public CopyRight extractCopyRightByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取软件著作权字段：OCR文本为空，返回默认实体");
            return buildDefaultCopyRight(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "copyright-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取软件著作权字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用软件著作权规则）
            String aiUserPrompt = String.format("""
                    请严格按照System Prompt中「软件著作权表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                    1. 核心字段：title（著作权名称）、certificateNo（证书号）、registerNo（登记号）、finishTime（开发完成时间）、puttime（获得时间）、copyrightOwner（著作权人）；
                    2. 证书号需保留“软著登字第XXXX号”完整格式，著作权类型固定为“计算机软件著作权”；
                    3. OCR文本：%s
                    """, ocrText);

            // 4. 调用AI提取（调用新增的extractCopyRightFields方法）
            log.info("AI提取软件著作权字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractCopyRightFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取软件著作权字段：AI返回空响应");
                return buildDefaultCopyRight(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取软件著作权字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为CopyRight（处理字段类型与合法性校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            CopyRight copyRight = convertJsonToCopyRight(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            copyRight = fillCopyRightDefaultValue(copyRight);

            log.info("AI提取软件著作权成功：著作权名称={}，证书号={}",
                    copyRight.getTitle(), copyRight.getCertificateNo());
            return copyRight;

        } catch (Exception e) {
            log.error("AI提取软件著作权字段失败", e);
            return buildDefaultCopyRight(pdfPath);
        }
    }


    // -------------------------- 软件著作权辅助方法 --------------------------
    /**
     * JSON对象转为CopyRight实体（处理类型转换与字段校验）
     */
    private CopyRight convertJsonToCopyRight(JSONObject jsonObj, String pdfPath) {
        CopyRight copyRight = new CopyRight();

        // 1. 基础字符串字段（含格式校验）
        copyRight.setTitle(jsonObj.getStr("title", null));
        copyRight.setCertificateNo(standardizeCertificateNo(jsonObj.getStr("certificateNo"))); // 证书号标准化
        copyRight.setRegisterNo(jsonObj.getStr("registerNo", null));
        copyRight.setCopyrightType("计算机软件著作权"); // 固定类型
        copyRight.setCopyrightOwner(jsonObj.getStr("copyrightOwner", null));
        copyRight.setDescription(jsonObj.getStr("description", ""));

        // 2. 数字字段（本人排名，处理空值）
        copyRight.setAuthorRank(jsonObj.getInt("authorRank", null));

        // 3. 日期字段（开发完成时间、获得时间，多格式匹配）
        copyRight.setFinishTime(parseCopyrightDate(jsonObj.getStr("finishTime")));
        copyRight.setPuttime(parseCopyrightDate(jsonObj.getStr("puttime"))); // 注意实体字段是puttime（避免拼写错误）

        // 4. 开发者信息（数组处理，补全默认值）
        copyRight.setAuthorInfo(parseAuthorInfo(jsonObj.getJSONArray("authorInfo")));

        // 5. 专属字段（AI无法提取，手动赋值）
        copyRight.setPdfPath(pdfPath); // PDF路径（关联文件）
        copyRight.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        copyRight.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        copyRight.setIsDelete(0); // 软删除默认“未删除”

        return copyRight;
    }

    /**
     * 软件著作权证书号标准化（补全“软著登字第”前缀，匹配正则格式）
     */
    private String standardizeCertificateNo(String certificateNo) {
        if (certificateNo == null || certificateNo.trim().isEmpty()) {
            return null;
        }
        String trimmedNo = certificateNo.trim();
        // 若缺少前缀，补全后校验；若已含前缀，直接校验
        if (!trimmedNo.startsWith("软著登字第")) {
            trimmedNo = "软著登字第" + trimmedNo;
        }
        Matcher matcher = CERTIFICATE_NO_PATTERN.matcher(trimmedNo);
        return matcher.find() ? matcher.group() : null; // 仅保留符合正则的格式
    }

    /**
     * 解析软件著作权日期（适配多种格式，转为Timestamp）
     */
    private Timestamp parseCopyrightDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        for (SimpleDateFormat sdf : COPYRIGHT_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                Date date = sdf.parse(dateStr);
                return new Timestamp(date.getTime());
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析软件著作权日期：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 解析开发者信息（补全身份、单位默认值）
     */
    private List<Author> parseAuthorInfo(JSONArray authorJsonArray) {
        List<Author> authorList = new ArrayList<>();
        if (authorJsonArray == null || authorJsonArray.isEmpty()) {
            return authorList;
        }

        for (Object obj : authorJsonArray) {
            JSONObject authorJson = JSONUtil.parseObj(obj);
            Author author = new Author();
            author.setRank(Integer.valueOf(authorJson.getStr("rank", "1"))); // 默认排序1

            author.setName(authorJson.getStr("name", ""));
            // 身份默认“其他”，单位默认“宁德师范学院信息工程学院”
            author.setIdentity(authorJson.getStr("identity", "其他"));
            author.setInstitution(authorJson.getStr("institution", "信息工程学院"));
            authorList.add(author);
        }
        return authorList;
    }

    /**
     * 补全软件著作权默认值（AI未返回或校验失败的字段）
     */
    private CopyRight fillCopyRightDefaultValue(CopyRight copyRight) {
        if (copyRight == null) {
            return new CopyRight();
        }
        // 1. 同步状态默认“是”
        if (copyRight.getSyncStatus() == null) {
            copyRight.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 著作权类型默认“计算机软件著作权”（防止AI遗漏）
        if (copyRight.getCopyrightType() == null || copyRight.getCopyrightType().trim().isEmpty()) {
            copyRight.setCopyrightType("计算机软件著作权");
        }
        // 3. 开发者信息默认空数组（避免NullPointerException）
        if (copyRight.getAuthorInfo() == null) {
            copyRight.setAuthorInfo(new ArrayList<>());
        }
        // 4. 备注默认空字符串
        if (copyRight.getDescription() == null) {
            copyRight.setDescription("");
        }
        // 5. 关联课题默认0（前端手动选择，数据库字段为Integer）
        if (copyRight.getProjectId() == null) {
            copyRight.setProjectId(0);
        }
        return copyRight;
    }

    /**
     * 构建软件著作权默认实体（容错用，含必要字段）
     */
    private CopyRight buildDefaultCopyRight(String pdfPath) {
        CopyRight defaultCopyRight = new CopyRight();
        // 专属字段赋值
        defaultCopyRight.setPdfPath(pdfPath);
        defaultCopyRight.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        defaultCopyRight.setIsDelete(0);
        // 默认值
        defaultCopyRight.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultCopyRight.setCopyrightType("计算机软件著作权");
        defaultCopyRight.setAuthorInfo(new ArrayList<>());
        defaultCopyRight.setDescription("");
        defaultCopyRight.setProjectId(0);
        return defaultCopyRight;
    }

    // ========================== 新增：著作解析核心方法 ==========================
    /**
     * 著作：从OCR文本AI提取字段，生成Publication实体
     * @param ocrText OCR文本（优先取著作封面+版权页，含名称、ISBN、出版社等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的Publication（含用户ID、PDF路径等必要字段）
     */
    public Publication extractPublicationByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取著作字段：OCR文本为空，返回默认实体");
            return buildDefaultPublication(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "publication-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取著作字段：临时MemoryId={}", tempMemoryId);

            String aiUserPrompt = String.format("""
                请严格按照System Prompt中「著作表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                1. 核心字段：
                   - title（著作名称）、isbn（ISBN，补全分隔符如978-7-115-58763-2）、role（身份：著作/编著/译著/其他）
                   - publisher（出版社）、publishDate（出版时间，yyyy-MM-dd）、wordCount（字数，单位千字）
                   - authorRank（本人排名，数字）、authorInfo（作者/编者列表，从OCR的"作者""编者""著者"提取，每个元素含：
                       rank：排序（数字字符串，如"1"，按作者顺序，不可重复）
                       name：中文名（必填，不可为空或"无"）
                       identity：身份（默认"老师"，可选"博士生/硕士生/本科生/其他"）
                       institution：单位（默认"宁德师范学院信息工程学院"）
                     无作者信息则返回空数组[]）
                2. 格式要求：
                   - 时间统一yyyy-MM-dd，数组必为数组（空则[]），缺失字段：字符串为null、数字为null、数组为空数组
                   - authorInfo中name不可为空，否则过滤该条数据
                3. OCR文本：%s
                """, ocrText);

            // 4. 调用AI提取（调用新增的extractPublicationFields方法）
            log.info("AI提取著作字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractPublicationFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取著作字段：AI返回空响应");
                return buildDefaultPublication(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取著作字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为Publication（处理字段类型与合法性校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            Publication publication = convertJsonToPublication(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            publication = fillPublicationDefaultValue(publication);

            log.info("AI提取著作成功：著作名称={}，ISBN={}，出版社={}",
                    publication.getTitle(), publication.getIsbn(), publication.getPublisher());
            return publication;

        } catch (Exception e) {
            log.error("AI提取著作字段失败", e);
            return buildDefaultPublication(pdfPath);
        }
    }


    // -------------------------- 著作辅助方法 --------------------------
    /**
     * JSON对象转为Publication实体（处理类型转换与字段校验）
     */
    private Publication convertJsonToPublication(JSONObject jsonObj, String pdfPath) {
        Publication publication = new Publication();

        // 1. 基础字符串字段（含格式标准化）
        publication.setTitle(jsonObj.getStr("title", null));
        publication.setIsbn(standardizeIsbn(jsonObj.getStr("isbn"))); // ISBN标准化
        publication.setRole(validateEnumValue(jsonObj.getStr("role"), VALID_ROLES));
        publication.setPublisher(jsonObj.getStr("publisher", null));
        publication.setType(validateEnumValue(jsonObj.getStr("type"), VALID_TYPES));
        publication.setLocation(jsonObj.getStr("location", null));
        publication.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 数字字段（排名、字数，处理单位与空值）
        publication.setAuthorRank(jsonObj.getInt("authorRank", null));
        publication.setWordCount(parseWordCount(jsonObj.getStr("wordCount"))); // 字数转换（千字）

        // 3. 日期字段（转换为Date，适配多种格式）
        publication.setPublishDate(parsePublicationDate(jsonObj.getStr("publishDate")));

        // 4. 作者信息（数组处理，补全默认值）
        publication.setAuthorInfo(parseAuthorInfo(jsonObj.getJSONArray("authorInfo")));

        // 5. 专属字段（AI无法提取，手动赋值）
        publication.setPdfPath(pdfPath); // PDF路径（关联文件）
        publication.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        publication.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        publication.setIsDelete(0); // 软删除默认“未删除”

        return publication;
    }

    /**
     * ISBN标准化（补全分隔符，匹配13位标准格式）
     */
    private String standardizeIsbn(String isbn) {
        if (isbn == null || isbn.trim().isEmpty()) {
            return null;
        }
        // 去除所有非数字字符
        String cleanIsbn = isbn.replaceAll("[^0-9]", "");
        // 仅处理13位ISBN（常见格式）
        if (cleanIsbn.length() != 13) {
            return isbn.trim(); // 非13位保留原始值
        }
        // 补全分隔符：978-组区号-出版者号-书名号-校验码
        return String.format("%s-%s-%s-%s-%s",
                cleanIsbn.substring(0, 3),    // 前缀（978/979）
                cleanIsbn.substring(3, 5),    // 组区号（如7=中国）
                cleanIsbn.substring(5, 10),   // 出版者号
                cleanIsbn.substring(10, 12),  // 书名号
                cleanIsbn.substring(12)       // 校验码
        );
    }

    /**
     * 解析字数（提取数字，单位为“千字”，支持小数）
     */
    private BigDecimal parseWordCount(String wordCountStr) {
        if (wordCountStr == null || wordCountStr.trim().isEmpty()) {
            return null;
        }
        // 提取数字（含小数），过滤“千字”“字”等单位
        String numberStr = wordCountStr.replaceAll("[^0-9.]", "").trim();
        if (numberStr.isEmpty() || !numberStr.matches("^\\d+(\\.\\d+)?$")) {
            return null;
        }
        // 若原始文本为“25800字”，转换为25.8千字
        if (wordCountStr.contains("字") && !wordCountStr.contains("千字") && numberStr.length() >= 4) {
            return new BigDecimal(numberStr).divide(new BigDecimal("1000"), 1, BigDecimal.ROUND_HALF_UP);
        }
        return new BigDecimal(numberStr);
    }

    /**
     * 解析著作出版时间（适配多种格式，转为Date）
     */
    private Date parsePublicationDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        for (SimpleDateFormat sdf : PUBLICATION_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                return sdf.parse(dateStr);
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析著作出版时间：{}（支持格式：yyyy-MM-dd、yyyy年MM月）", dateStr);
        return null;
    }




    /**
     * 补全著作默认值（AI未返回或校验失败的字段）
     */
    private Publication fillPublicationDefaultValue(Publication publication) {
        if (publication == null) {
            return new Publication();
        }
        // 1. 同步状态默认“是”
        if (publication.getSyncStatus() == null) {
            publication.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 身份默认“著作”（未识别或无效值时）
        if (publication.getRole() == null || !VALID_ROLES.contains(publication.getRole())) {
            publication.setRole("著作");
        }
        // 3. 著作类型默认“非教材”（未识别或无效值时）
        if (publication.getType() == null || !VALID_TYPES.contains(publication.getType())) {
            publication.setType("非教材");
        }
        // 4. 作者信息默认空数组（避免NullPointerException）
        if (publication.getAuthorInfo() == null) {
            publication.setAuthorInfo(new ArrayList<>());
        }
        // 5. 备注默认空字符串
        if (publication.getRemarks() == null) {
            publication.setRemarks("");
        }
        // 6. 关联课题默认0（前端手动选择，数据库字段为Integer）
        if (publication.getProjectId() == null) {
            publication.setProjectId(0);
        }
        if (publication.getAuthorInfo() == null || publication.getAuthorInfo().isEmpty()) {
            List<Author> defaultAuthors = new ArrayList<>();
            Author defaultAuthor = new Author();
            defaultAuthor.setRank(1);
            defaultAuthor.setName("");
            defaultAuthor.setIdentity("老师");
            defaultAuthor.setInstitution("信息工程学院");
            defaultAuthors.add(defaultAuthor);
            publication.setAuthorInfo(defaultAuthors);
        }
        return publication;
    }

    /**
     * 构建著作默认实体（容错用，含必要字段）
     */
    private Publication buildDefaultPublication(String pdfPath) {
        Publication defaultPub = new Publication();
        // 专属字段赋值
        defaultPub.setPdfPath(pdfPath);
        defaultPub.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        defaultPub.setIsDelete(0);
        // 默认值
        defaultPub.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultPub.setRole("著作");
        defaultPub.setType("非教材");
        defaultPub.setAuthorInfo(new ArrayList<>());
        defaultPub.setRemarks("");
        defaultPub.setProjectId(0);
        return defaultPub;
    }

    // ========================== 新增：继续教育材料解析核心方法 ==========================
    /**
     * 继续教育材料：从OCR文本AI提取字段，生成ContinueEducationMaterial实体
     * @param ocrText OCR文本（优先取培训证书封面/证明核心页，含标题、培训单位、课时等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的ContinueEducationMaterial（含用户ID、PDF路径等必要字段）
     */
    public ContinueEducationMaterial extractContinueEducationMaterialByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取继续教育材料字段：OCR文本为空，返回默认实体");
            return buildDefaultCEM(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "cem-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取继续教育材料字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用继续教育材料规则）
            String aiUserPrompt = String.format("""
                    请严格按照System Prompt中「继续教育材料表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                    1. 核心字段：title（材料标题）、trainingUnit（培训单位）、hours（课时）、score（评分/等级）、obtainTime（获得时间）；
                    2. 课时需转换为数字（含小数，如“4.5小时”→4.5），获得时间补全为yyyy-MM-dd格式；
                    3. OCR文本：%s
                    """, ocrText);

            // 4. 调用AI提取（调用新增的extractContinueEducationMaterialFields方法）
            log.info("AI提取继续教育材料字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractContinueEducationMaterialFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取继续教育材料字段：AI返回空响应");
                return buildDefaultCEM(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取继续教育材料字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为ContinueEducationMaterial（处理字段类型与校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            ContinueEducationMaterial cem = convertJsonToCEM(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            cem = fillCEMDefaultValue(cem);

            log.info("AI提取继续教育材料成功：材料标题={}，培训单位={}，课时={}",
                    cem.getTitle(), cem.getTrainingUnit(), cem.getHours());
            return cem;

        } catch (Exception e) {
            log.error("AI提取继续教育材料字段失败", e);
            return buildDefaultCEM(pdfPath);
        }
    }


    // -------------------------- 继续教育材料辅助方法 --------------------------
    /**
     * JSON对象转为ContinueEducationMaterial实体（处理类型转换与字段校验）
     */
    private ContinueEducationMaterial convertJsonToCEM(JSONObject jsonObj, String pdfPath) {
        ContinueEducationMaterial cem = new ContinueEducationMaterial();

        // 1. 基础字符串字段
        cem.setTitle(jsonObj.getStr("title", null));
        cem.setTrainingUnit(standardizeTrainingUnit(jsonObj.getStr("trainingUnit"))); // 培训单位标准化
        cem.setScore(jsonObj.getStr("score", null));
        cem.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 数字字段（课时：提取数字，处理单位）
        cem.setHours(parseHours(jsonObj.getStr("hours")));

        // 3. 日期字段（获得时间：多格式匹配）
        cem.setObtainTime(parseCEMDate(jsonObj.getStr("obtainTime")));

        // 4. 专属字段（AI无法提取，手动赋值）
        cem.setPdfPath(pdfPath); // PDF路径（关联文件）
        cem.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        cem.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        cem.setIsDelete(0); // 软删除默认“未删除”

        return cem;
    }

    /**
     * 培训单位标准化（补全省份/机构全称，如“省教育厅”→“福建省教育厅”）
     */
    private String standardizeTrainingUnit(String unit) {
        if (unit == null || unit.trim().isEmpty()) {
            return null;
        }
        String trimmedUnit = unit.trim();
        // 适配宁德本地常见培训单位，补全前缀
        if (trimmedUnit.startsWith("省教育厅") && !trimmedUnit.contains("福建")) {
            return "福建省教育厅";
        } else if (trimmedUnit.startsWith("教师发展中心") && !trimmedUnit.contains("宁德师范学院")) {
            return "宁德师范学院教师发展中心";
        }
        return trimmedUnit;
    }

    /**
     * 解析课时（提取数字，过滤单位，转为BigDecimal）
     */
    private BigDecimal parseHours(String hoursStr) {
        if (hoursStr == null || hoursStr.trim().isEmpty()) {
            return null;
        }
        // 提取字符串中的数字（含小数）
        Matcher matcher = HOURS_PATTERN.matcher(hoursStr);
        if (!matcher.find()) {
            log.warn("解析课时：未提取到数字，hoursStr={}", hoursStr);
            return null;
        }
        String numberStr = matcher.group();
        try {
            return new BigDecimal(numberStr);
        } catch (Exception e) {
            log.error("解析课时：数字转换失败，numberStr={}", numberStr, e);
            return null;
        }
    }

    /**
     * 解析继续教育材料日期（适配多种格式，转为Timestamp）
     */
    private Timestamp parseCEMDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        // 处理模糊日期（如“2024年7月”→补全为“2024年7月1日”）
        String processedDateStr = dateStr.trim().replaceAll("年(\\d{1,2})月$", "年$1月1日");

        for (SimpleDateFormat sdf : CEM_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                Date date = sdf.parse(processedDateStr);
                return new Timestamp(date.getTime());
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析继续教育材料获得时间：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 补全继续教育材料默认值（AI未返回的字段）
     */
    private ContinueEducationMaterial fillCEMDefaultValue(ContinueEducationMaterial cem) {
        if (cem == null) {
            return new ContinueEducationMaterial();
        }
        // 1. 同步状态默认“是”
        if (cem.getSyncStatus() == null) {
            cem.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 备注默认空字符串
        if (cem.getRemarks() == null) {
            cem.setRemarks("");
        }
        // 3. 关联课题ID默认空字符串（数据库字段为String）
        if (cem.getProjectId() == null) {
            cem.setProjectId("");
        }
        // 4. 软删除默认0
        if (cem.getIsDelete() == null) {
            cem.setIsDelete(0);
        }
        return cem;
    }

    /**
     * 构建继续教育材料默认实体（容错用，含必要字段）
     */
    private ContinueEducationMaterial buildDefaultCEM(String pdfPath) {
        ContinueEducationMaterial defaultCEM = new ContinueEducationMaterial();
        // 专属字段赋值
        defaultCEM.setPdfPath(pdfPath);
        defaultCEM.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        defaultCEM.setIsDelete(0);
        // 默认值
        defaultCEM.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultCEM.setRemarks("");
        defaultCEM.setProjectId("");
        return defaultCEM;
    }

    // ========================== 新增：档案资料解析核心方法 ==========================
    /**
     * 档案资料：从OCR文本AI提取字段，生成Certification实体
     * @param ocrText OCR文本（优先取档案证书封面/核心页，含标题、证书类型、编号等）
     * @param pdfPath PDF文件路径（前端上传后传递，关联实体）
     * @return 填充后的Certification（含用户ID、PDF路径等必要字段）
     */
    public Certification extractCertificationByAi(String ocrText, String pdfPath) {
        // 1. 前置校验：OCR文本为空→返回默认实体
        if (ocrText == null || ocrText.trim().isEmpty()) {
            log.warn("AI提取档案资料字段：OCR文本为空，返回默认实体");
            return buildDefaultCertification(pdfPath);
        }

        try {
            // 2. 生成临时MemoryId（避免AI上下文冲突）
            String tempMemoryId = "certification-ai-extract-" + UUID.randomUUID();
            log.debug("AI提取档案资料字段：临时MemoryId={}", tempMemoryId);

            // 3. 构造AI请求指令（明确调用档案资料规则）
            String aiUserPrompt = String.format("""
                    请严格按照System Prompt中「档案资料表单字段提取规则」，从以下OCR文本提取字段，仅返回JSON字符串（无多余文本）：
                    1. 核心字段：title（档案标题）、certificateType（证书类型）、obtainTime（获得时间）、certificateNumber（证书编号）、issuingInstitution（授予机构）；
                    2. 证书类型仅从“学历证书/学位证书/职业资格证书/荣誉证书/培训证书/其他”中选择，证书编号过滤空格与乱码；
                    3. OCR文本：%s
                    """, ocrText);

            // 4. 调用AI提取（调用新增的extractCertificationFields方法）
            log.info("AI提取档案资料字段：开始调用，OCR文本长度={}字符", ocrText.length());
            String aiJsonResponse = xiaoNingExtractor.extractCertificationFields(tempMemoryId, aiUserPrompt);

            // 5. 校验AI响应
            if (aiJsonResponse == null || aiJsonResponse.trim().isEmpty()) {
                log.error("AI提取档案资料字段：AI返回空响应");
                return buildDefaultCertification(pdfPath);
            }

            // 6. 清理AI响应（去除引号、转义符）
            String cleanAiJson = aiJsonResponse.trim()
                    .replaceAll("^[\"']|[\"']$", "")
                    .replaceAll("\\\\n|\\\\r", "")
                    .replaceAll("\\\\", "");
            log.debug("AI提取档案资料字段：清理后JSON={}", cleanAiJson);

            // 7. 解析JSON为Certification（处理字段类型与校验）
            if (!JSONUtil.isTypeJSON(cleanAiJson)) {
                throw new Exception("AI返回非合法JSON：" + cleanAiJson);
            }
            JSONObject jsonObj = JSONUtil.parseObj(cleanAiJson);
            Certification certification = convertJsonToCertification(jsonObj, pdfPath);

            // 8. 补全默认值（AI未返回的字段）
            certification = fillCertificationDefaultValue(certification);

            log.info("AI提取档案资料成功：档案标题={}，证书类型={}，授予机构={}",
                    certification.getTitle(), certification.getCertificateType(), certification.getIssuingInstitution());
            return certification;

        } catch (Exception e) {
            log.error("AI提取档案资料字段失败", e);
            return buildDefaultCertification(pdfPath);
        }
    }


    // -------------------------- 档案资料辅助方法 --------------------------
    /**
     * JSON对象转为Certification实体（处理类型转换与字段校验）
     */
    private Certification convertJsonToCertification(JSONObject jsonObj, String pdfPath) {
        Certification certification = new Certification();

        // 1. 基础字符串字段（含标准化处理）
        certification.setTitle(jsonObj.getStr("title", null));
        certification.setCertificateType(validateCertType(jsonObj.getStr("certificateType"))); // 证书类型校验
        certification.setCertificateNumber(standardizeCertNumber(jsonObj.getStr("certificateNumber"))); // 证书编号标准化
        certification.setIssuingInstitution(standardizeInstitution(jsonObj.getStr("issuingInstitution"))); // 授予机构标准化
        certification.setRemarks(jsonObj.getStr("remarks", ""));

        // 2. 日期字段（获得时间：多格式匹配）
        certification.setObtainTime(parseCertDate(jsonObj.getStr("obtainTime")));

        // 3. 专属字段（AI无法提取，手动赋值）
        certification.setPdfPath(pdfPath); // PDF路径（关联文件）
        certification.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null); // 当前用户ID
        certification.setSyncStatus(DEFAULT_SYNC_STATUS); // 默认同步
        certification.setIsDelete(0); // 软删除默认“未删除”

        return certification;
    }

    /**
     * 证书类型校验（仅保留合法值，无效值返回null）
     */
    private String validateCertType(String certType) {
        if (certType == null || certType.trim().isEmpty()) {
            return null;
        }
        String trimmedType = certType.trim();
        // 模糊映射（如“软考证书”→“职业资格证书”）
        if (trimmedType.contains("资格") || trimmedType.contains("软考") || trimmedType.contains("技能")) {
            return "职业资格证书";
        } else if (trimmedType.contains("学历") || trimmedType.contains("毕业")) {
            return "学历证书";
        } else if (trimmedType.contains("学位")) {
            return "学位证书";
        } else if (trimmedType.contains("荣誉") || trimmedType.contains("优秀")) {
            return "荣誉证书";
        } else if (trimmedType.contains("培训") || trimmedType.contains("进修")) {
            return "培训证书";
        }
        // 精确匹配合法值
        return VALID_CERT_TYPES.contains(trimmedType) ? trimmedType : null;
    }

    /**
     * 证书编号标准化（过滤空格、乱码，保留字母/数字/短横线）
     */
    private String standardizeCertNumber(String certNum) {
        if (certNum == null || certNum.trim().isEmpty()) {
            return null;
        }
        // 提取符合格式的编号（5-20位字母/数字/短横线）
        Matcher matcher = CERT_NUM_PATTERN.matcher(certNum.replaceAll("\\s+", ""));
        return matcher.find() ? matcher.group() : certNum.replaceAll("\\s+", "");
    }

    /**
     * 授予机构标准化（补全宁德本地机构全称，如“宁师”→“宁德师范学院”）
     */
    private String standardizeInstitution(String institution) {
        if (institution == null || institution.trim().isEmpty()) {
            return null;
        }
        String trimmedInst = institution.trim();
        // 适配宁德本地常见机构，补全全称
        if (trimmedInst.equals("宁师") || trimmedInst.equals("宁德师院")) {
            return "宁德师范学院";
        } else if (trimmedInst.equals("省教育厅")) {
            return "福建省教育厅";
        } else if (trimmedInst.equals("市人社局")) {
            return "宁德市人力资源和社会保障局";
        } else if (trimmedInst.equals("人社部")) {
            return "人力资源和社会保障部";
        }
        return trimmedInst;
    }

    /**
     * 解析档案资料日期（适配多种格式，转为Date）
     */
    private Date parseCertDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return null;
        }
        // 处理模糊日期（如“2024年6月”→补全为“2024年6月1日”）
        String processedDateStr = dateStr.trim().replaceAll("年(\\d{1,2})月$", "年$1月1日");

        for (SimpleDateFormat sdf : CERT_DATE_FORMATS) {
            try {
                sdf.setLenient(false); // 严格校验格式
                return sdf.parse(processedDateStr);
            } catch (ParseException e) {
                continue; // 格式不匹配，尝试下一种
            }
        }
        log.warn("无法解析档案资料获得时间：{}（支持格式：yyyy-MM-dd、yyyy年MM月dd日）", dateStr);
        return null;
    }

    /**
     * 补全档案资料默认值（AI未返回或校验失败的字段）
     */
    private Certification fillCertificationDefaultValue(Certification certification) {
        if (certification == null) {
            return new Certification();
        }
        // 1. 同步状态默认“是”
        if (certification.getSyncStatus() == null) {
            certification.setSyncStatus(DEFAULT_SYNC_STATUS);
        }
        // 2. 证书类型默认“其他”（未识别或无效值时）
        if (certification.getCertificateType() == null || !VALID_CERT_TYPES.contains(certification.getCertificateType())) {
            certification.setCertificateType("其他");
        }
        // 3. 备注默认空字符串
        if (certification.getRemarks() == null) {
            certification.setRemarks("");
        }
        // 4. 关联课题默认0（前端手动选择，数据库字段为Integer）
        if (certification.getProjectId() == null) {
            certification.setProjectId(0);
        }
        // 5. 软删除默认0
        if (certification.getIsDelete() == null) {
            certification.setIsDelete(0);
        }
        return certification;
    }

    /**
     * 构建档案资料默认实体（容错用，含必要字段）
     */
    private Certification buildDefaultCertification(String pdfPath) {
        Certification defaultCert = new Certification();
        // 专属字段赋值
        defaultCert.setPdfPath(pdfPath);
        defaultCert.setUserId(UserHolder.getUser() != null ? UserHolder.getUser().getUserId() : null);
        defaultCert.setIsDelete(0);
        // 默认值
        defaultCert.setSyncStatus(DEFAULT_SYNC_STATUS);
        defaultCert.setCertificateType("其他");
        defaultCert.setRemarks("");
        defaultCert.setProjectId(0);
        return defaultCert;
    }
}
